//demo19.cpp  文件传输，客户端程序

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<fstream>
using namespace std;



class ctcpclient    //TCP通讯的客户端类
{
public:
    int m_clientfd;                 //客户端的socket，-1表示未连接成功或链接已断开，>=0表示有效的socket
    string m_ip;                    //服务端的IP/域名
    unsigned short m_port;          //通讯端口

    ctcpclient():m_clientfd(-1){}   //构造函数

    //向服务器发起链接请求，成功返回true，失败返回false
    bool connect(const string &in_ip, const unsigned short in_port)
    {
        if(m_clientfd != -1)        //如果socket已连接，直接返回失败
           return false;

        m_ip = in_ip;
        m_port = in_port;           //把服务端的IP和端口保存到成员变量中

        //第1步: 创建客户端的socket
         m_clientfd = socket(AF_INET, SOCK_STREAM, 0);
         if(m_clientfd == -1)
         {
            return false;
         }

        //第 2 步:向服务器发起链接请求。
        struct sockaddr_in servaddr;                //用于存放协议、端口和IP地址的结构体
        memset(&servaddr, 0, sizeof(servaddr));     //初始化
        servaddr.sin_family = AF_INET;              //1)协议族，IPV4为AF_INET
        servaddr.sin_port = htons(m_port);          //2)指定服务端的通信端口

        struct hostent* h;              //用于存放服务器IP地址(大端序)的结构体指针
        if((h = gethostbyname(m_ip.c_str())) == nullptr)     //把域名/主机名/字符串格式的IP转换成结构体
        {
            ::close(m_clientfd);
            m_clientfd = -1;
            return false;
        }
        memcpy(&servaddr.sin_addr, h->h_addr, h->h_length);      //3)指定服务端的IP(大端序)              
        //servaddr.sin_addr.s_addr=inet_addr(argv[1]);       //3)指定服务端的IP，只能用IP熊用域名和主机名>    ，不推荐
        
        if(::connect(m_clientfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)   //向服务端发起连接请求
        {   //connect()库函数与成员函数重名，调用库函数加::即可
            ::close(m_clientfd);
            m_clientfd = -1;
            return false;
        }
        return true;
    }

    //向服务端发送请求报文(字符串)，成功返回true, 失败返回false
    bool send(const string &buffer)     //buffer不要用const char,因为它只支持c语言风格字符串，string不行
    {                                   //const string 可以两者兼顾
        if(m_clientfd == -1)
            return false;               //如果soket未连接，直接返回失败

        if((::send(m_clientfd, buffer.data(), buffer.size(), 0)) <= 0)
            return false;
        return true;
    }

    //向服务端发送请求报文(二进制数据)，成功返回true, 失败返回false
    bool send(const string &buffer)     //buffer不要用const char,因为它只支持c语言风格字符串，string不行
    {                                   //const string 可以两者兼顾
        if(m_clientfd == -1)
            return false;               //如果soket未连接，直接返回失败

        if((::send(m_clientfd, buffer, size, 0)) <= 0)
            return false;
        return true;
    }


    //接收服务端的报文，成功返回true， 失败返回false。
    //buffer-存放接收到的报文的内容，maxlen-本次接收报文的最大长度。
    bool recv(string &buffer, const size_t maxlen)
    {
        buffer.clear();         //清空容器
        buffer.resize(maxlen);  //把容器大小设为maxlen
        int readn = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);   //直接操作buffer的内存
        if(readn <= 0)
        {
            buffer.clear();
            return false;
        }
        buffer.resize(readn);   //重置buffer实际大小
        return true;
    }


    //断开与服务端的连接
    bool close()
    {
        if(m_clientfd == -1)
            return false;       //如果socket是未连接状态，直接返回失败
        ::close(m_clientfd);
        m_clientfd = -1;
        return true;
    }

    //向服务端发送文件内容
    bool sendfile(const string &filename, const size_t filesize)
    {
        //以二进制的方式打开文件
        ifstream fin(filename, ios::binary);
        if(fin.is_open() == false)
        {
            cout << "打开文件 " << filename << "失败。" << endl;
            return -1;
        }

        int onread = 0;         //每次调用fin.read()时打算读取的字节数
        int totalbytes = 0;     //从文件中已读取的字节总数
        char buffer[7];         //存放每次读取数据的buffer,一般实际开发Buffer大小在1000-5000
                                //比较能发挥磁盘性能

        while(true)
        {
            memset(buffer, 0, sizeof(buffer));

            //计算本次应该读取的字节数，如果剩余的数据超过7个字节，就读7字节
            if(filesize - totalbytes > 7)  onread=7;
            else  onread=filesize - totalbytes;

            //从文件中读取数据
            fin.read(buffer, onread);

            //把读取到的数据发送给对端
            if(send(buffer, onread) == false)  return false;

            //计算文件已读取的字节总数，如果文件已读完，跳出循环
            totalbytes = totalbytes + onread;
            if(totalbytes == filesize)  break;
        }
        return true;
    }


    ~ctcpclient(){close();}
};


int main(int argc, char  *argv[])
{
    if(argc != 5)
    {
        cout << "Using:./demo19 服务端的IP 服务端的端口 文件名 文件大小"<< endl;
        cout << "Example:./demo19 192.168.227.129 5005 aaa.txt 2424\n\n";
        return -1;
    }

    ctcpclient tcpclient;
    if(tcpclient.connect(argv[1], atoi(argv[2])) == false)      //向服务端发起连接请求
    {
        perror("connect()");
        return -1;
    }

    //以下是发送文件的流程。

    //1）把待传输文件名和文件的大小告诉服务端
    struct st_fileinfo          //文件结构体
    {
        char filename[256];     //文件名
        int filesize;           //文件大小  
    }fileinfo;

    memset(&fileinfo, 0, sizeof(fileinfo));     //初始化结构体
    strcpy(fileinfo.filename, argv[3]); //文件名
    fileinfo.filesize = atoi(argv[4]);          //文件大小

    //把文件信息的结构体发送给服务端
    if(tcpclient.send(&fileinfo, sizeof(fileinfo)) == false)
    {
        perror("send" );
        return -1;
    }
    cout << "发送文件信息的结构体 " << fileinfo.filename << "(" << fileinfo.filesize << ")." << endl;



    //2）等待服务端的确认报文(文件名和文件的大小的确认)
    string buffer;
    if(tcpclient.recv(buffer, 2) == false)
    {
        perror("recv()");
        return -1;
    }
    if(buffer != "ok")
    {
        cout << "服务端没有回复ok。" << endl;
        return -1;
    }



    //3) 发送文件内容(封装成成员数)
    if(tcpclient.sendfile(fileinfo.filename, fileinfo.filesize) == false)
    {
        perror("sendfile()");  return -1;
    }


    //4）等待服务端的确认报文(服务端已接收完文件)
    if(tcpclient.recv(buffer, 2) == false)
    {
        perror("recv()");
        return -1;
    }
    if(buffer != "ok")
    {
        cout << "发送文件内容失败。" << endl;
        return -1;
    }
    cout << "发送文件内容成功。" << endl;


    return 0;
}

